package com.cdks.wuxiu.api.service;

import com.cdks.wuxiu.api.component.constant.Constant;
import com.cdks.wuxiu.api.component.constant.ScoreEnum;
import com.cdks.wuxiu.api.model.dto.CallScoreInfoDTO;
import com.cdks.wuxiu.api.model.dto.UserAnswerStatisDTO;
import com.cdks.wuxiu.api.model.dto.UserScoreDTO;
import com.cdks.wuxiu.api.component.cdks.kmsg.SingleMessage;
import com.cdks.wuxiu.api.component.constant.AuthLevelEnum;
import com.cdks.wuxiu.api.component.constant.CallEnum;
import com.cdks.wuxiu.api.service.mysql.CallScoreInfoService;
import com.cdks.wuxiu.api.service.mysql.CallSheetInfoService;
import com.cdks.wuxiu.api.service.mysql.DailySystemInfoService;
import com.cdks.wuxiu.api.service.mysql.DailyUserInfoService;
import com.cdks.wuxiu.api.service.mysql.ScoreAwardConfigService;
import com.cdks.wuxiu.api.service.mysql.UserBaseInfoService;
import com.cdks.wuxiu.api.service.mysql.UserBillInfoService;
import com.cdks.wuxiu.api.service.mysql.UserLoginInfoService;
import com.cdks.wuxiu.api.service.mysql.UserScoreInfoService;
import com.cdks.wuxiu.api.service.mysql.UserStatisInfoService;
import com.cdks.wuxiu.api.service.mysql.UserWalletInfoService;
import com.cdks.wuxiu.api.service.redis.RedisService;
import com.cdks.wuxiu.api.util.SmsUtils;
import com.cdks.wuxiu.api.util.TimeUtils;
import com.cdks.wuxiu.entity.message.CallPrivateData;
import com.cdks.wuxiu.entity.message.ChatPrivateData;
import com.cdks.wuxiu.entity.mysql.CallScoreInfo;
import com.cdks.wuxiu.entity.mysql.CallSheetInfo;
import com.cdks.wuxiu.entity.mysql.ScoreAwardConfig;
import com.cdks.wuxiu.entity.mysql.UserBaseInfo;
import com.cdks.wuxiu.entity.mysql.UserWalletInfo;

import cn.hutool.core.thread.ThreadUtil;
import lombok.extern.slf4j.Slf4j;

import java.time.LocalTime;
import java.util.Date;
import java.util.List;
import java.util.Set;

import javax.annotation.Resource;

import org.springframework.stereotype.Service;


/**
 * 通话信息表 服务实现类
 */
@Slf4j
@Service
public class CallServiceImpl implements CallService {
    @Resource
    private UserBaseInfoService userBaseInfoService;
    @Resource
    private UserLoginInfoService userLoginInfoService;
    @Resource
    private CallSheetInfoService callSheetInfoService;
    @Resource
    private CallScoreInfoService callScoreInfoService;    
    @Resource
    private UserWalletInfoService userWalletInfoService;
    @Resource
    private UserBillInfoService userBillInfoService;
    @Resource
    private UserStatisInfoService userStatisInfoService;
    @Resource
    private UserScoreInfoService userScoreInfoService;
    @Resource
    private ScoreAwardConfigService scoreAwardConfigService;    
    @Resource
    private DailyUserInfoService dailyUserInfoService; 
    @Resource
    DailySystemInfoService dailySystemInfoService;   
    @Resource
    private RedisService redisService;


    private int sendCallReport(long sender, long recver[], long caller, long duration, int why)
    {
        ChatPrivateData info  = new ChatPrivateData();
        info.setCallerId(caller);
        info.setDuration(duration);
        info.setEndType(why); 
        SingleMessage.sendChatMessage(sender, recver, Constant.MSG_TYPE_CALL, Constant.ENABLE_NO, info); 
        
        return 0;
    }

    private int sendCallReport(long sender, long recver, long caller, long duration, int why)
    {
        sendCallReport(sender, new long[]{recver}, caller, duration, why); 
        
        return 0;
    }

    private int saveInviteBill(long payBill, long earnBill, long payReal, long payScore, long earnReal, long payerId, long earnerId, int payType, int earnType, long sheetId)
    {
        int ret  = userBillInfoService.saveBill(payBill, earnBill, payReal, payScore, earnReal, payerId, earnerId, payType, earnType, sheetId, "用户邀请");
        if (ret < 0)
        {
            log.error("invite save bill failed");

            return -1;
        }

        return 0;
    }

    private int saveCallBill(CallSheetInfo sheet, int payType, int earnType)
    {
        if ((sheet.getPayBean() == 0) && (sheet.getPayScore() == 0) && (sheet.getEarnCost() == 0))
        {
            return 0;
        }

        int ret  = userBillInfoService.saveBill(sheet.getPayCost(), sheet.getEarnCost(), sheet.getPayBean(), sheet.getPayScore(), sheet.getEarnCost(), sheet.getCallerId(), sheet.getAnswerId(), payType, earnType, sheet.getId(), "语音通话");
        if (ret < 0)
        {
            log.error("call save bill failed");

            return -1;
        }

        return 0;
    }

    private int saveAwardBill(long cost, long payerId, long earnerId, long sheetId, String remark)
    {
        int ret  = userBillInfoService.saveBill(cost, cost, 0, cost, cost, payerId, earnerId, Constant.COST_TYPE_SCORE, Constant.COST_TYPE_SCORE, sheetId, remark);
        if (ret < 0)
        {
            log.error("call reward save bill failed");

            return -1;
        }

        return 0;
    }

    private int rewardScore(long userId, UserBaseInfo inviter, long cost, ScoreEnum type, long sheetId)
    {
        //获取积分奖励配置
        ScoreAwardConfig config  = scoreAwardConfigService.getByType(type.getCode());
        if ((config == null) || (config.getStartTime().compareTo(LocalTime.now()) > 0) || (config.getStopTime().compareTo(LocalTime.now()) < 0))
        {
            return -1;
        }

        long score  = 0;        
        score  = (cost * config.getScore() / 1000) * 10;
        score  = (score < 10) ? 10 : score;

        //给邀请人发放奖励
        long expire  = TimeUtils.getTimeSecond() + config.getExpire();
        userScoreInfoService.addScore(inviter.getUserId(), score, type.getCode(), expire, type.getDesc());
        //保存邀请人积分账单
        saveAwardBill(score, userId, inviter.getUserId(), sheetId, type.getDesc());

        //给用户发送通知
        ChatPrivateData data  = new ChatPrivateData();
        data.setContent("您有" + score + "积分到账，可消费也可提现，有效期至" + TimeUtils.getLocalDateTime("yyyy-MM-dd HH:mm:ss", expire));
        SingleMessage.sendChatMessage(Constant.USERID_SYSTEM, inviter.getUserId(), Constant.MSG_TYPE_TEXT, Constant.ENABLE_NO, data);

        return 0;
    }

    private int rewardInviter(long callerId, long answerId, long payCost, long earnCost, long sheetId)
    {
        //给主叫的邀请人发放奖励
        if (payCost > 0)
        {
            //查找用户的邀请人
            UserBaseInfo inviter  = userBaseInfoService.getInviterInfo(callerId);

            if ((inviter != null) && (inviter.getUserId() != callerId) && (inviter.getType() < Constant.ACCOUNT_TYPE_PLAT))
            {
                rewardScore(callerId, inviter, payCost, ScoreEnum.TYPE_INVITED_CALL_EXPEND, sheetId);
            }
        }

        //给被叫的邀请人发放奖励
        if (earnCost > 0)
        {
            //查找用户的邀请人
            UserBaseInfo inviter  = userBaseInfoService.getInviterInfo(answerId);

            if ((inviter != null) && (inviter.getUserId() != answerId) && (inviter.getType() < Constant.ACCOUNT_TYPE_PLAT))
            {
                rewardScore(answerId, inviter, earnCost, ScoreEnum.TYPE_INVITED_CALL_INCOME, sheetId);
                inviteCharge(answerId, inviter, earnCost, ScoreEnum.TYPE_INVITED_CALL_INCOME, sheetId);
            }
        }

        return 0;
    }

    private int inviteCharge(long userId, UserBaseInfo inviter, long cost, ScoreEnum type, long sheetId)
    {
        long coin  = 0;        
        coin  = (cost * 20 / 1000) * 10;
        if (coin == 0)
        {
            return -2;
        }

        //保存账单
        saveInviteBill(coin, coin, coin, 0, coin, userId, inviter.getUserId(), Constant.COST_TYPE_COIN, Constant.COST_TYPE_COIN, sheetId);
        //被叫计费
        userWalletInfoService.decCoin(userId, coin, Constant.STATIS_TYPE_INVITE, Constant.PAY_MODE_FIRST);
        //邀请人计费
        userWalletInfoService.addCoin(inviter.getUserId(), coin, Constant.STATIS_TYPE_INVITE);

        //给用户发送通知
        ChatPrivateData data  = new ChatPrivateData();
        data.setContent("邀请收益" + coin + "金币已到账");
        SingleMessage.sendChatMessage(Constant.USERID_SYSTEM, inviter.getUserId(), Constant.MSG_TYPE_TEXT, Constant.ENABLE_NO, data);

        return 0;
    }

    private int updateAnswerInfo(CallSheetInfo sheet, UserBaseInfo answer)
    {
        //以话单创建时间为准更新被叫每日通话呼入次数、接听次数和接听时长
        dailyUserInfoService.updateAnswerInfo(new Date(sheet.getCreateTime() * 1000), sheet.getAnswerId(), sheet.getDuration());
        UserAnswerStatisDTO statis  = dailyUserInfoService.getAnswerStatis(sheet.getAnswerId());
        //更新被叫接通率和认证等级
        int authLevel  = answer.getAuthLevel();
        userBaseInfoService.updateListenerInfo(answer, statis);
        if (answer.getAuthRole() == Constant.AUTH_ROLE_TALKER)
        {
            ChatPrivateData data  = new ChatPrivateData();
            data.setContent("因您接通率太低，已被取消认证。若有疑问请联系客服。");
            SingleMessage.sendChatMessage(Constant.USERID_SYSTEM, answer.getUserId(), Constant.MSG_TYPE_TEXT, Constant.ENABLE_NO, data);
        }
        else
        {
            if (authLevel < answer.getAuthLevel())
            {
                ChatPrivateData data  = new ChatPrivateData();
                data.setContent("恭喜您认证等级已升级为：" + AuthLevelEnum.getDescByCode(answer.getAuthLevel()));
                SingleMessage.sendChatMessage(Constant.USERID_SYSTEM, answer.getUserId(), Constant.MSG_TYPE_TEXT, Constant.ENABLE_NO, data);
            }
            else if (authLevel > answer.getAuthLevel())
            {
                ChatPrivateData data  = new ChatPrivateData();
                data.setContent("您的认证等级已发生变更，当前为：" + AuthLevelEnum.getDescByCode(answer.getAuthLevel()));
                SingleMessage.sendChatMessage(Constant.USERID_SYSTEM, answer.getUserId(), Constant.MSG_TYPE_TEXT, Constant.ENABLE_NO, data);
            }
        }

        return 0;
    }

    @Override
    public CallSheetInfo getSheetByUcid(String ucid) {
        return callSheetInfoService.getByUcid(ucid);
    }

    @Override
    public CallSheetInfo getSheetByUcidWithCaller(String ucid, long caller) {
        return callSheetInfoService.getByUcidWithCaller(ucid, caller);
    }

    @Override
    public CallSheetInfo getSheetByUcidWithAnswer(String ucid, long answer) {
        return callSheetInfoService.getByUcidWithAnswer(ucid, answer);
    }

    @Override
    public CallSheetInfo getSheetByUcidWithAnyUser(String ucid, long userId) {
        return callSheetInfoService.getByUcidWithAnyUser(ucid, userId);
    }

    @Override
    public int callout(CallSheetInfo sheet, UserBaseInfo caller) {
        int ret  = callSheetInfoService.callout(sheet);
        if (ret < 0)
        {
            return -1;
        }

        //更新用户在线状态为忙碌
        userBaseInfoService.updateOnlineStatus(sheet.getCallerId(), Constant.USER_STATUS_BUSY);
        userBaseInfoService.updateOnlineStatus(sheet.getAnswerId(), Constant.USER_STATUS_BUSY);

        //不在这里统计被叫呼入次数，因为随机匹配发起呼叫时被叫未知。所以被叫通话信息统计统一放在挂断时更新
        if (caller.getType() == Constant.ACCOUNT_TYPE_USER)
        {
            dailySystemInfoService.addCallCount();
        }

        return 0;
    }

    @Override
    public int ringing(CallSheetInfo sheet) {
        //更新话单
        sheet.setRingTime(TimeUtils.getTimeSecond());
        int ret  = callSheetInfoService.ringing(sheet);
        if (ret < 0)
        {
            return -1;
        }

        //更新用户在线状态为忙碌
        userBaseInfoService.updateOnlineStatus(sheet.getAnswerId(), Constant.USER_STATUS_BUSY);
        
        return 0;
    }

    @Override
    public int answer(CallSheetInfo sheet) {
        long nowTime  = TimeUtils.getTimeSecond();
        //如果被叫未能调用ringing（例如，网络不好），则未能更新振铃时间，所以在这里补充更新
        if (sheet.getRingTime() == 0)
        {
            sheet.setRingTime(nowTime);
        }

        //因为一键倾诉抢接模式，不同被叫扣费价格不同，如果先更改话单后扣费，扣费失败时需要重新修改话单被叫，所以先扣费再更改话单
        if ((sheet.getPayBean() > 0) && (sheet.getFreeNum() == 0))
        {
            //主叫计费一分钟（按预付费方式计费）
            int ret  = userWalletInfoService.decBean(sheet.getCallerId(), sheet.getPayBean(), Constant.STATIS_TYPE_CONSUME, Constant.PAY_MODE_FIRST);
            if (ret < 0)
            {
                //一键倾诉余额不足直接返回
                if ((sheet.getType() == Constant.CALL_TYPE_RANDOM_AUDIO) || (sheet.getType() == Constant.CALL_TYPE_RANDOM_VIDEO))
                {
                    log.info("call answer charge failed: cost = " + sheet.getPayBean() + ", ret = " + ret + ", sheet = " + sheet);

                    return -2;
                }
                else
                {
                    log.error("call answer charge failed: cost = " + sheet.getPayBean() + ", ret = " + ret + ", sheet = " + sheet);

                    //主叫计费失败，强制挂断通话
                    sheet.setPayBean(0L);
                    sheet.setPayCost(0L);
                    sheet.setRemark("系统：被叫接听，主叫计费失败");
                    sheet.setStatus(Constant.CALL_STATUS_STOPPED + Constant.CALL_ENDTYPE_ERROR);
                    callSheetInfoService.hangup(sheet);

                    //发送通话报告
                    sendCallReport(sheet.getCallerId(), sheet.getAnswerId(), sheet.getCallerId(), sheet.getDuration(), Constant.CALL_ENDTYPE_SYSTEM);
                    sendCallReport(sheet.getAnswerId(), sheet.getCallerId(), sheet.getCallerId(), sheet.getDuration(), Constant.CALL_ENDTYPE_SYSTEM);

                    //给主叫发挂断信令
                    CallPrivateData data  = new CallPrivateData();
                    data.setUcid(sheet.getUcid());
                    data.setCost(sheet.getPayCost());
                    data.setType(Constant.CALL_TYPE_SINGLE_AUDIO);
                    data.setWhy(CallEnum.END_BY_SYSTEM.getCode());
                    data.setHint("余额不足，请先充值"); 
                    data.setDuration(sheet.getDuration());    
                    //发送系统挂断消息
                    SingleMessage.sendCallMessage(Constant.USERID_SYSTEM, sheet.getCallerId(), Constant.MSG_TYPE_HANGUP, Constant.ENABLE_NO, data);

                    return -3;
                }
            }
        }
        
        //更新话单
        int ret  = callSheetInfoService.answer(sheet);
        if (ret < 0)
        {
            if ((sheet.getType() == Constant.CALL_TYPE_RANDOM_AUDIO) || (sheet.getType() == Constant.CALL_TYPE_RANDOM_VIDEO))
            {
                log.info("call answer failed: sheet = " + sheet + ", ret = " + ret);
            }
            else
            {
                log.error("call answer failed: sheet = " + sheet + ", ret = " + ret);
            }

            return -1;
        }

        log.info("call answer : sheet = " + sheet);

        //更新用户在线状态为忙碌
        userBaseInfoService.updateOnlineStatus(sheet.getAnswerId(), Constant.USER_STATUS_BUSY);

        //初始化通话保持时间
        redisService.setCallKeepTime(sheet.getCallerId(), nowTime, sheet.getUcid());
        redisService.setCallKeepTime(sheet.getAnswerId(), nowTime, sheet.getUcid());

        return 0;
    }

    @Override
    public int hangup(CallSheetInfo sheet) {
        //更新用户在线状态为空闲
        userBaseInfoService.updateOnlineStatus(sheet.getCallerId(), Constant.USER_STATUS_IDLE);
        userBaseInfoService.updateOnlineStatus(sheet.getAnswerId(), Constant.USER_STATUS_IDLE);

        long nowTime     = TimeUtils.getTimeSecond();
        if (sheet.getAnswerId() < Constant.USERID_SYSTEM)
        {
            sheet.setStopTime(nowTime);

            log.info("call hangup : sheet = " + sheet);
    
            //更改话单状态。如果失败说明主叫重复挂断，一键倾诉为抢接模式只可能主叫挂断
            callSheetInfoService.hangup(sheet);

            return -1;
        }
        
        long startTime   = sheet.getStartTime();
        long callerTime  = redisService.getCallKeepTime(sheet.getCallerId(), sheet.getUcid());
        long answerTime  = redisService.getCallKeepTime(sheet.getAnswerId(), sheet.getUcid());
        long chargeTime  = Math.min(callerTime, answerTime);
        long stopTime    = (chargeTime > 0) ? Math.min(nowTime, chargeTime + 60) : nowTime;
        long duration    = (startTime == 0) ? 0 : stopTime - startTime;

        long minutes  = (duration + 59) / 60;
        if (minutes > sheet.getFreeNum())
        {
            minutes  = minutes - sheet.getFreeNum();
        }
        else
        {
            minutes  = 0;
        }

        long payBill     = minutes * sheet.getPayPrice();
        long earnBill    = minutes * sheet.getEarnPrice();
        long cost        = payBill - sheet.getPayCost();
        log.info("call hangup : callerTime = " + callerTime + ", answerTime = " + answerTime + ", duration = " + duration + ", cost = " + cost + ", sheet = " + sheet);
        
        if (cost < 0)
        {
            int ret  = callSheetInfoService.hangup(sheet);
            //保存账单
            if (ret == 0)
            {
                saveCallBill(sheet, Constant.COST_TYPE_BEAN, Constant.COST_TYPE_COIN);
            }

            //发送管理通知
            SmsUtils.sendManageNotice("通话计费异常");
            //发送通话报告
            sendCallReport(sheet.getCallerId(), sheet.getAnswerId(), sheet.getCallerId(), sheet.getDuration(), Constant.CALL_ENDTYPE_ERROR);
            sendCallReport(sheet.getAnswerId(), sheet.getCallerId(), sheet.getCallerId(), sheet.getDuration(), Constant.CALL_ENDTYPE_ERROR);

            log.error("call hangup charge failed: cost = " + cost + ", sheet = " + sheet);

            return 0;
        }

        long costBean    = 0;
        long costScore   = 0;
        long minScoreId  = 0;
        long maxScoreId  = 0;
        if (cost > 0)
        {
            UserScoreDTO score  = userScoreInfoService.getBalanceByUser(sheet.getCallerId(), stopTime);
            UserWalletInfo wallet  = userWalletInfoService.getByUserId(sheet.getCallerId());
            log.info("call hangup : UserScoreDTO = " + score + ", stopTime=" + stopTime + ", wallet=" + wallet);
            if (wallet.getBean() < cost)
            {
                costBean    = wallet.getBean();
                costScore   = cost - wallet.getBean();
                minScoreId  = score.getMinId();
                maxScoreId  = score.getMaxId();
            }
            else
            {
                costBean   = cost;
                costScore  = 0;
            }
        }

        sheet.setStopTime(nowTime);
        sheet.setDuration(duration);
        sheet.setPayCost(payBill);
        sheet.setEarnCost(earnBill);
        sheet.setPayBean(costBean + sheet.getPayBean());
        sheet.setPayScore(costScore + sheet.getPayScore());

        log.info("call hangup : costBean = " + costBean + ", costScore = " + costScore + ", cost = " + cost + ", sheet = " + sheet);

        //更改话单状态。如果失败说明已挂断（例如：主被叫同时挂断，后挂断的一方就会失败）
        int ret  = callSheetInfoService.hangup(sheet);
        if (ret < 0)
        {
            //已挂断，直接返回
            return -2;
        }

        //发送通话报告
        sendCallReport(sheet.getCallerId(), sheet.getAnswerId(), sheet.getCallerId(), sheet.getDuration(), sheet.getStatus() - Constant.CALL_STATUS_STOPPED);
        sendCallReport(sheet.getAnswerId(), sheet.getCallerId(), sheet.getCallerId(), sheet.getDuration(), sheet.getStatus() - Constant.CALL_STATUS_STOPPED);

        //已认证用户接听免费通话固定计费一分钟
        if ((sheet.getFreeNum() > 0) && (duration >= Constant.TIME_VALUE_30_SECONDS))
        {
            UserBaseInfo answer  = userBaseInfoService.getInfoById(sheet.getAnswerId());
            if (answer.getAuthRole() > 0)
            {
                long score  = Constant.PRICE_CALL_MINAUDIO;

                //给被叫发放奖励
                long expire  = TimeUtils.getTimeSecond() + Constant.TIME_VALUE_24_HOURS;
                userScoreInfoService.addScore(answer.getUserId(), score, ScoreEnum.TYPE_CALL_FREE_SCORE.getCode(), expire, ScoreEnum.TYPE_CALL_FREE_SCORE.getDesc());
                //保存被叫积分账单
                saveAwardBill(score, Constant.USERID_SYSTEM, answer.getUserId(), sheet.getId(), ScoreEnum.TYPE_CALL_FREE_SCORE.getDesc());

                //给用户发送通知
                ChatPrivateData data  = new ChatPrivateData();
                data.setContent("您有" + score + "积分奖励到账，可消费也可提现，有效期至" + TimeUtils.getLocalDateTime("yyyy-MM-dd HH:mm:ss", expire));
                SingleMessage.sendChatMessage(Constant.USERID_SYSTEM, answer.getUserId(), Constant.MSG_TYPE_TEXT, Constant.ENABLE_NO, data);

                if (cost == 0)
                {
                    //振铃时间为0说明被叫离线
                    if (sheet.getRingTime() > 0)
                    {
                        //更新被叫信息
                        updateAnswerInfo(sheet, answer);
                    }

                    return 0;
                }
            }
        }

        //通话时长不足30秒不计费，并且需要退还主叫已扣费用（因为被叫接听就会扣主叫一分钟）。
        //因为第一分钟不能用积分，所以只需要退心豆
        if ((duration < Constant.TIME_VALUE_30_SECONDS) && (sheet.getPayBean() > 0))
        {
            //给主叫退费
            userWalletInfoService.addBean(sheet.getCallerId(), sheet.getPayBean(), Constant.STATIS_TYPE_CONSUME);

            //更新话单中主被叫的实际计费信息。第一分钟不能用积分抵扣，所以只需要修改心豆
            sheet.setPayBean(0L);
            sheet.setPayCost(0L);
            sheet.setEarnCost(0L);
            callSheetInfoService.update(sheet);
        }

        //先扣心豆
        if (costBean > 0)
        {
            ret  = userWalletInfoService.decBean(sheet.getCallerId(), costBean, Constant.STATIS_TYPE_CONSUME, Constant.PAY_MODE_LATER);
            if (ret < 0)
            {
                sheet.setPayBean(sheet.getPayBean() - costBean);
                callSheetInfoService.update(sheet);

                log.error("update bean failed: cost = " + costBean + ", ret = " + ret + ", sheet = " + sheet);

                //发送管理通知
                SmsUtils.sendManageNotice("通话计费失败-1");
            }
        }

        //再扣积分
        if (costScore > 0)
        {
            ret  = userScoreInfoService.decByUserId(sheet.getCallerId(), minScoreId, maxScoreId, costScore, stopTime);
            if (ret < 0)
            {
                sheet.setPayScore(sheet.getPayScore() - costScore);
                callSheetInfoService.update(sheet);

                log.error("update score failed : score = " + costScore + ", ret = " + ret + ", sheet = " + sheet);

                //发送管理通知
                SmsUtils.sendManageNotice("通话计费失败-2");
            }
        }

        //保存账单，被叫计费，并更新用户信息（一键倾诉默认被叫非用户ID，因此需要判断被叫）
        if (sheet.getAnswerId() > Constant.USERID_SYSTEM)
        {
            finish(sheet);
        }

        return 0;
    }

    @Override
    public int keepAlive(CallSheetInfo sheet) {
        //更新用户在线状态为忙碌
        userBaseInfoService.updateOnlineStatus(sheet.getCallerId(), Constant.USER_STATUS_BUSY);
        userBaseInfoService.updateOnlineStatus(sheet.getAnswerId(), Constant.USER_STATUS_BUSY);

        //计费
        long nowTime     = TimeUtils.getTimeSecond();
        long callerTime  = redisService.getCallKeepTime(sheet.getCallerId(), sheet.getUcid());
        long answerTime  = redisService.getCallKeepTime(sheet.getAnswerId(), sheet.getUcid());
        long chargeTime  = Math.min(callerTime, answerTime);
        long stopTime    = Math.min(nowTime, chargeTime + 60);
        long duration    = stopTime - sheet.getStartTime();        

        long minutes     = (duration + 59) / 60;
        if (minutes > sheet.getFreeNum())
        {
            minutes  = minutes - sheet.getFreeNum();
        }
        else
        {
            minutes  = 0;
        }

        long payBill     = minutes * sheet.getPayPrice();
        long cost        = payBill - sheet.getPayCost();
        log.info("call keep: callerTime = " + callerTime + ", answerTime = " + answerTime + ", duration = " + duration + ", cost = " + cost + ", sheet = " + sheet);

        if (cost < 0)
        {
            sheet.setRemark("系统：通话保持，计费异常");
            sheet.setStatus(Constant.CALL_STATUS_STOPPED + Constant.CALL_ENDTYPE_ERROR);
            callSheetInfoService.hangup(sheet);

            //发送管理通知
            SmsUtils.sendManageNotice("通话计费异常");

            //发送通话报告
            sendCallReport(sheet.getCallerId(), sheet.getAnswerId(), sheet.getCallerId(), sheet.getDuration(), Constant.CALL_ENDTYPE_ERROR);
            sendCallReport(sheet.getAnswerId(), sheet.getCallerId(), sheet.getCallerId(), sheet.getDuration(), Constant.CALL_ENDTYPE_ERROR);

            return -1;
        }

        //双方时差超过2分钟自动挂断
        long diff  = callerTime - answerTime;
        if ((diff < -120) || (diff > 120))
        {
            sheet.setRemark("系统：通话保持，用户已掉线");
            sheet.setStatus(Constant.CALL_STATUS_STOPPED + Constant.CALL_ENDTYPE_SYSTEM);
            callSheetInfoService.hangup(sheet);

            //发送通话报告
            sendCallReport(sheet.getCallerId(), sheet.getAnswerId(), sheet.getCallerId(), sheet.getDuration(), Constant.CALL_ENDTYPE_SYSTEM);
            sendCallReport(sheet.getAnswerId(), sheet.getCallerId(), sheet.getCallerId(), sheet.getDuration(), Constant.CALL_ENDTYPE_SYSTEM);

            return -2;
        }

        //主叫计费（按预付费方式计费）
        //记录更新前已支付费用，用该值作为更新话单的条件，避免满足扣费条件时一方在扣费过程中，另一方取到脏话单重复扣费的情况
        long payedCost   = sheet.getPayCost();
        long costBean    = 0;
        long costScore   = 0;
        long minScoreId  = 0;
        long maxScoreId  = 0;

        if (cost > 0)
        {
            UserScoreDTO score  = userScoreInfoService.getBalanceByUser(sheet.getCallerId(), stopTime);
            UserWalletInfo wallet  = userWalletInfoService.getByUserId(sheet.getCallerId());
            log.info("call keep : UserScoreDTO = " + score + ", stopTime=" + stopTime + ", wallet=" + wallet);
            if (wallet.getBean() < cost)
            {
                costBean    = wallet.getBean();
                costScore   = cost - wallet.getBean();
                minScoreId  = score.getMinId();
                maxScoreId  = score.getMaxId();
            }
            else
            {
                costBean   = cost;
                costScore  = 0;
            }
        }

        //更新话单信息
        sheet.setPayCost(payBill);
        sheet.setPayBean(costBean + sheet.getPayBean());
        sheet.setPayScore(costScore + sheet.getPayScore());
        sheet.setDuration(duration);

        log.info("call keep: costBean = " + costBean + ", costScore = " + costScore + ", cost = " + cost + ", sheet = " + sheet);

        //失败原因1：因为是先取话单后更改状态，所以可能存在主被叫取到相同的话单同时计费的情况，此时必然有一方会计费失败
        //失败原因2：如果一方挂断另一方 keepAlive，也会计费失败
        long ret  = callSheetInfoService.keepAlive(sheet, payedCost);
        if (ret < 0)
        {
            return 0;
        }

        //先扣心豆
        if (costBean > 0)
        {
            ret  = userWalletInfoService.decBean(sheet.getCallerId(), costBean, Constant.STATIS_TYPE_CONSUME, Constant.PAY_MODE_FIRST);
            if (ret < 0)
            {
                log.error("call keep dec bean failed: cost = " + costBean + ", ret = " + ret + ", sheet = " + sheet);

                sheet.setPayBean(sheet.getPayBean() - costBean);
                sheet.setPayScore(sheet.getPayScore() - costScore);//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!注意，这里心豆扣费失败直接返回了，所以要修改话单实际支付积分
                sheet.setRemark("系统：通话保持，主叫心豆计费失败");
                sheet.setStatus(Constant.CALL_STATUS_STOPPED + Constant.CALL_ENDTYPE_ERROR);
                sheet.setEarnCost(minutes * sheet.getEarnPrice());
                callSheetInfoService.hangup(sheet);

                //发送管理通知
                SmsUtils.sendManageNotice("通话计费失败-3");
                //发送通话报告
                sendCallReport(sheet.getCallerId(), sheet.getAnswerId(), sheet.getCallerId(), sheet.getDuration(), Constant.CALL_ENDTYPE_ERROR);
                sendCallReport(sheet.getAnswerId(), sheet.getCallerId(), sheet.getCallerId(), sheet.getDuration(), Constant.CALL_ENDTYPE_ERROR);

                return -3;
            }
        }

        //再扣积分
        if (costScore > 0)
        {
            ret  = userScoreInfoService.decByUserId(sheet.getCallerId(), minScoreId, maxScoreId, costScore, stopTime);
            if (ret < 0)
            {
                log.error("call keep dec score failed : score = " + costScore + ", ret = " + ret + ", sheet = " + sheet);

                sheet.setPayScore(sheet.getPayScore() - costScore);
                sheet.setRemark("系统：通话保持，主叫积分计费失败");
                sheet.setStatus(Constant.CALL_STATUS_STOPPED + Constant.CALL_ENDTYPE_ERROR);
                sheet.setEarnCost(minutes * sheet.getEarnPrice());
                callSheetInfoService.hangup(sheet);

                //发送管理通知
                SmsUtils.sendManageNotice("通话计费失败-4");
                //发送通话报告
                sendCallReport(sheet.getCallerId(), sheet.getAnswerId(), sheet.getCallerId(), sheet.getDuration(), Constant.CALL_ENDTYPE_ERROR);
                sendCallReport(sheet.getAnswerId(), sheet.getCallerId(), sheet.getCallerId(), sheet.getDuration(), Constant.CALL_ENDTYPE_ERROR);

                return -4;
            }            
        }

        return 0;
    }

    @Override
    public CallSheetInfo getBusyCallByCaller(long userId) {
        return callSheetInfoService.getBusyCallByCaller(userId);
    }

    @Override
    public CallSheetInfo getBusyCallByAnswer(long userId) {
        return callSheetInfoService.getBusyCallByAnswer(userId);
    }

    @Override
    public CallSheetInfo getBusyCallAnyUser(long caller, long answer) {
        return callSheetInfoService.getBusyCallAnyUser(caller, answer);
    }

    @Override
    public CallSheetInfo getStartedCallAnyUser(long callerId, long answerId) {
        return callSheetInfoService.getStartedCallAnyUser(callerId, answerId);
    }

    @Override
    public CallSheetInfo getStartedCallBothUser(long callerId, long answerId) {
        return callSheetInfoService.getStartedCallBothUser(callerId, answerId);
    }

    @Override
    public CallSheetInfo getPayedCallBothUser(long callerId, long answerId) {
        return callSheetInfoService.getPayedCallBothUser(callerId, answerId);
    }

    @Override
    public List<CallSheetInfo> getFreeCallByCaller(long callerId) {
        return callSheetInfoService.getFreeCallByCaller(callerId);
    }

    @Override
    public int setCallKeepTime(long userId, long time, String ucid) {
        redisService.setCallKeepTime(userId, time, ucid);
        
        return 0;
    }

    @Override
    public int getCallKeepTime(long userId, String ucid) {
        return redisService.getCallKeepTime(userId, ucid);
    }

    @Override
    public int finish(CallSheetInfo sheet)
    {
        long callerId  = sheet.getCallerId();
        long answerId  = sheet.getAnswerId();

        //保存账单
        saveCallBill(sheet, Constant.COST_TYPE_BEAN, Constant.COST_TYPE_COIN);
        //发放积分奖励
        rewardInviter(callerId, answerId, sheet.getPayBean(), sheet.getEarnCost(), sheet.getId());
        //被叫计费
        userWalletInfoService.addCoin(answerId, sheet.getEarnCost(), Constant.STATIS_TYPE_INWORK);

        //更新被叫信息。振铃时间为0说明被叫离线
        UserBaseInfo answer  = userBaseInfoService.getInfoById(answerId);
        if ((answer.getAuthRole() > Constant.AUTH_ROLE_TALKER) && (sheet.getRingTime() > 0))
        {
            updateAnswerInfo(sheet, answer);
        }

        return 0;
    }

    @Override
    public int callEvalute(CallSheetInfo sheet, int score, String content) {
        CallScoreInfo scoreInfo  = new CallScoreInfo();
        scoreInfo.setCreateTime(TimeUtils.getTimeSecond());
        scoreInfo.setSheetId(sheet.getId());
        scoreInfo.setUserId(sheet.getCallerId());
        scoreInfo.setDestId(sheet.getAnswerId());
        scoreInfo.setScore(score);

        if ((score > 2) && (content == null))
        {
            scoreInfo.setStatus(Constant.AUDIT_STATUS_PASS);
        }

        if (content != null)
        {
            scoreInfo.setContent(content.trim());
        }

        boolean ret = callScoreInfoService.save(scoreInfo);
        if (ret == false)
        {
            return -1;
        }

        userStatisInfoService.addScoreCount(sheet.getAnswerId(), score);

        return 0;
    }

    @Override
    public List<CallScoreInfoDTO> getCallScoreList(int page, long userId) {
        return callScoreInfoService.getList(page, userId);
    }

    @Override
    public UserBaseInfo selectCallee(long selfId, int userType, int role, int sex, int balance, Set<Long> excludes) {
        UserBaseInfo callee  = callSheetInfoService.selectCallee(selfId, userType, role, sex, balance, excludes);
        if (callee != null)
        {
            excludes.add(callee.getUserId());
        }

        return callee;        
    }

    @Override
    public UserBaseInfo getRandAnswer(long selfId, int userType, int role, int sex, int balance, Set<Long> excludes) {
        UserBaseInfo answer  = callSheetInfoService.getRandAnswer(selfId, userType, role, sex, balance, excludes);
        if (answer != null)
        {
            excludes.add(answer.getUserId());
        }

        return answer;
    }

    @Override
    public List<UserBaseInfo> getNoticeCallee(long caller, int userType, int role, long answer) {
        //获取30分钟内已通知过的用户
        Set<String> excludes  = redisService.getNoticedCallee(caller);
        //排除自己
        excludes.add(String.valueOf(caller));
        //排除当前被叫
        excludes.add(String.valueOf(answer));

        List<UserBaseInfo> userList  = callSheetInfoService.getNoticeCallee(caller, userType, role, excludes);
        //记录已通知过的用户
        for (UserBaseInfo userInfo : userList)
        {
            redisService.saveNoticedCallee(caller, userInfo.getUserId());
        }

        return userList;
    }

    @Override
    public List<UserBaseInfo> getRandCallee(long caller, int userType, int role, int sex) {
        return callSheetInfoService.getRandCallee(caller, userType, role, sex);     
    }

    @Override
    public CallSheetInfo getSheetBySipId(String sipId) {
        return callSheetInfoService.getBySipId(sipId);
    }

    @Override
    public int hangupCallByStatus(CallSheetInfo sheet, int status) {
        sheet.setStopTime(TimeUtils.getTimeSecond());
        int ret  = callSheetInfoService.updateSheetByStatus(sheet, status);
        if (ret == 0)
        {
            log.info("call hangup success: sheet = " + sheet);

            //更新用户在线状态为空闲
            userBaseInfoService.updateOnlineStatus(sheet.getCallerId(), Constant.USER_STATUS_BUSY, Constant.USER_STATUS_IDLE);
            userBaseInfoService.updateOnlineStatus(sheet.getAnswerId(), Constant.USER_STATUS_BUSY, Constant.USER_STATUS_IDLE);
        }
        else
        {
            log.info("call hangup failed: sheet = " + sheet);
        }

        return ret;
    }

    @Override
    public int hangupRandCall(CallSheetInfo sheet, int why, String hint) {
        ThreadUtil.execAsync(() -> {
            Set<String> ids  = redisService.getRandCall(sheet.getCallerId());
            if (ids.size() > 0)
            {
                //被叫接听的时候会调用该方法，所以要把接听的被叫移除。在通话结束的时候会把接听被叫的状态设置为空闲
                ids.remove(String.valueOf(sheet.getAnswerId()));

                //将该主叫匹配的其他忙碌被叫状态设置为空闲
                userBaseInfoService.updateOnlineStatus(ids, Constant.USER_STATUS_BUSY, Constant.USER_STATUS_IDLE);
                //删除该主叫匹配的所有被叫
                redisService.deleteRandCall(sheet.getCallerId());
            }
            
            if (ids.size() == 0)
            {
                return;
            }

            int i  = 0;
            long[] tos = new long[ids.size()];
            for (String callee : ids)
            {
                long userId  = Long.valueOf(callee);

                tos[i ++] = userId;
                //将该通话从该主叫匹配的其他被叫的待接听列表中删除
                redisService.removeWaitCall(userId, sheet.getUcid());
            }

            //给被叫发送挂断信令
            CallPrivateData data  = new CallPrivateData();
            data.setUcid(sheet.getUcid());
            data.setCost(0L);
            data.setType(sheet.getType());
            data.setWhy(why);
            data.setHint(hint);
            data.setDuration(0L);
            SingleMessage.sendCallMessage(sheet.getCallerId(), tos, Constant.MSG_TYPE_HANGUP, Constant.ENABLE_NO, data);
            //为了心诉离线被叫打开APP能显示未接通话消息
            SingleMessage.sendChatMessage(sheet.getCallerId(), tos, Constant.MSG_TYPE_HANGUP, Constant.ENABLE_NO, data);
            //给被叫发送通话报告
            sendCallReport(sheet.getCallerId(), tos, sheet.getCallerId(), sheet.getDuration(), why);
            //发送离线通知
            SingleMessage.sendOfflineNotice(tos, "未接来电", "您刚才有一次通话未能接听，快去联系一下用户吧~~");            
        });            

        return 0;
    }

    @Override
    public int updateListenerInfo(CallSheetInfo sheet, UserBaseInfo userInfo) {
        //更新被叫信息
        updateAnswerInfo(sheet, userInfo);

        return 0;
    }
}
